/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 2008-2011, Salzburg Research. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * - Redistributions of source code must retain the above copyright notice,
 * this list of conditions and the following disclaimer.
 * - Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 * - Neither the name of the KiWi Project nor the names of its contributors
 * may be used to endorse or promote products derived from this software
 * without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 *
 * Contributor(s):
 * sschaffe
 *
 * File Info:
 * User: sschaffe
 * Date: 2011/01/13
 * Time: 12:32
 *
 * Project: KiWi2, kiwi-core module
 */

package kiwi.core.servlet;

import com.google.common.io.ByteStreams;
import eu.medsea.mimeutil.MimeType;
import eu.medsea.mimeutil.MimeUtil2;
import kiwi.core.api.config.ConfigurationService;
import kiwi.core.api.search.SolrIndexingService;
import kiwi.core.qualifiers.cache.KiWiCache;
import kiwi.core.services.search.solr.program.parser.src.ParseException;
import net.sf.ehcache.Cache;
import net.sf.ehcache.Element;
import org.apache.commons.configuration.Configuration;
import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.configuration.PropertiesConfiguration;
import org.slf4j.Logger;

import javax.inject.Inject;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.JarURLConnection;
import java.net.URL;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;

/**
 * This filter is used by KiWi for initialisation of the KiWi system on startup of the server. It does not perform
 * any actual filtering. For this purpose, a listener would have been better, but CDI in Jetty does not support
 * injection into listeners, so we "abuse" a filter for this purpose. Filters always get initialised before servlets,
 * so by adding the KiWiFilter as the first entry into web.xml, we can ensure that the KiWi initialisation is done
 * before everything else.
 * <p/>
 * User: sschaffe
 */
@SuppressWarnings({"CdiManagedBeanInconsistencyInspection"})
public class KiWiResourceFilter implements Filter {

    @Inject
    private Logger log;

    @Inject
    private ConfigurationService configurationService;

	//must be injected because program should be parsed
	@Inject
	private SolrIndexingService solrIndexingService;

    @Inject @KiWiCache("resource-cache")
    private Cache data_cache;

    /**
     * Used for detecting the mime type of resources contained in KiWi modules
     */
    private MimeUtil2 mimeUtil;

    private boolean caching, proxy;

    Map<String,String> resourceMap;


    /**
     * Called by the web container to indicate to a filter that it is being placed into
     * service. The servlet container calls the init method exactly once after instantiating the
     * filter. The init method must complete successfully before the filter is asked to do any
     * filtering work. <br><br>
     * <p/>
     * The web container cannot place the filter into service if the init method either<br>
     * 1.Throws a ServletException <br>
     * 2.Does not return within a time period defined by the web container
     */
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        log.info("LMF {} starting up ... ", configurationService.getConfiguration("kiwi.version"));

        String kiwiHome = filterConfig.getInitParameter("kiwi.home");
        if(kiwiHome != null) {
            System.setProperty("kiwi.home",kiwiHome);
        }

        // should module resources be cached or served from the jar file?
        String cachingCfg = filterConfig.getInitParameter("kiwi.resourceCaching");
        if("true".equals(cachingCfg)) {
            caching = true;
        } else if("false".equals(cachingCfg)) {
            caching = false;
        } else {
            caching = configurationService.getBooleanConfiguration("kiwi.resourceCaching",false);;
        }

        String proxyCfg = filterConfig.getInitParameter("kiwi.resourceProxy");
        if("true".equals(proxyCfg)) {
            proxy = true;
        } else if("false".equals(proxyCfg)) {
            proxy = false;
        } else  {
            proxy = configurationService.getBooleanConfiguration("kiwi.resourceProxy",true);
        }





        // find all kiwi-module.properties and check whether they contain a baseurl property to map module web
        // resources to a certain path prefix; if yes, store the prefix and jar URL in the map for lookup and
        // resource resolving by the filter
        this.resourceMap = new HashMap<String, String>();
        try {
            Enumeration<URL> modulePropertiesEnum = this.getClass().getClassLoader().getResources("kiwi-module.properties");

            while(modulePropertiesEnum.hasMoreElements()) {
                URL moduleUrl = modulePropertiesEnum.nextElement();

                Configuration moduleProperties = null;
                try {
                    moduleProperties = new PropertiesConfiguration(moduleUrl);

                    if(moduleProperties.containsKey("baseurl")) {
                        String path = moduleProperties.getString("baseurl");
                        JarURLConnection conn = (JarURLConnection)moduleUrl.openConnection();
                        URL jarUrl = conn.getJarFileURL();


                        log.debug("mapping resource path {} to module {}",path,jarUrl);

                        resourceMap.put(path,jarUrl.toString());

                    }


                } catch (ConfigurationException e) {
                    log.error("configuration exception: {}",e.getMessage());
                }

            }


        } catch (IOException e) {
            log.error("I/O error while trying to load kiwi-module.properties file: {}",e.getMessage());
        }


        // detect the mime type of resources
        this.mimeUtil = new MimeUtil2();
        this.mimeUtil.registerMimeDetector("eu.medsea.mimeutil.detector.ExtensionMimeDetector");

		//init SolrIndex
		try {
			solrIndexingService.initialize();
			log.info("SOLR Index initialized");
		} catch (ParseException e) {
			log.error("Could not initialize SOLR Index: {}",e.getMessage());
		}

        log.info("KiWi Core Version {} started successfully.", configurationService.getConfiguration("kiwi.version"));
    }

    /**
     * The <code>doFilter</code> method of the Filter is called by the container
     * each time a request/response pair is passed through the chain due
     * to a client request for a resource at the end of the chain. The FilterChain passed in to this
     * method allows the Filter to pass on the request and response to the next entity in the
     * chain.<p>
     * A typical implementation of this method would follow the following pattern:- <br>
     * 1. Examine the request<br>
     * 2. Optionally wrap the request object with a custom implementation to
     * filter content or headers for input filtering <br>
     * 3. Optionally wrap the response object with a custom implementation to
     * filter content or headers for output filtering <br>
     * 4. a) <strong>Either</strong> invoke the next entity in the chain using the FilterChain object (<code>chain.doFilter()</code>), <br>
     * * 4. b) <strong>or</strong> not pass on the request/response pair to the next entity in the filter chain to block the request processing<br>
     * * 5. Directly set headers on the response after invocation of the next entity in the filter chain.
     */
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        URL url = null;
        String prefix = null, path = null;
        if (request instanceof HttpServletRequest) {
            url    = new URL(((HttpServletRequest)request).getRequestURL().toString());
            prefix = ((HttpServletRequest)request).getContextPath();
            if(url.getPath().startsWith(prefix)) {
                path = url.getPath().substring(prefix.length());
            }
        }
        long duration, starttime = System.currentTimeMillis();


        if(path != null) {
            ResourceCacheEntry data = null;

            // check the request path prefix whether it matches with one of the prefixes in the prefix mapping; if yes,
            // lookup the resource as follows:
            // 1. look in the cache using the path of the request url as cache key
            // 2. if not found: look in the jar file by mapping the resource to the correct jar file and retrieving it
            //    from the "web" folder contained therein
            // 3. if not found: proceed with the chain by calling chain.doFilter()
            if(data_cache.isKeyInCache(path)) {
                data = (ResourceCacheEntry) data_cache.get(path).getObjectValue();
            } else {
                for(String key : resourceMap.keySet()) {
                    if(path.startsWith(key)) {

                        // the name of the resource inside the jar file
                        String entryPath    = path.substring(key.length());
                        // map "/" to /index.html
                        if(entryPath.endsWith("/") || entryPath.equals("")) {
                            entryPath += "index.html";
                        }

                        // the base URL of the jar file in the file system
                        String jarUrlBase   = resourceMap.get(key);

                        // the JAR URL of the resource inside the jar file
                        String jarUrlEntry  = "jar:" + jarUrlBase + "!/web" + ( entryPath.startsWith("/") ? entryPath : "/" + entryPath);


                        try {
                            URL jarUrl = new URL(jarUrlEntry);

                            byte[] bytes = ByteStreams.toByteArray(jarUrl.openStream());

                            data = new ResourceCacheEntry(bytes,bytes.length,getMimeType(jarUrl));

                            log.debug("retrieved resource {} (mime type {}, length {} bytes)",new Object[] {jarUrlEntry, data.getContentType(),data.getLength()});

                            putInCache(path,data);

                        } catch(IOException ex) {
                            log.debug("error while trying to retrieve resource {}: {}",jarUrlEntry,ex.getMessage());

                            putInCache(path,new ResourceCacheEntry(null,0,null));
                        }
                   }
                }

            }

            // if no: proceed with the chain by calling chain.doFilter()
            if(data != null && data.getLength() > 0) {
                HttpServletResponse hresponse = (HttpServletResponse) response;

                if(proxy) {
                    hresponse.setDateHeader("Expires", System.currentTimeMillis()+3600*1000);
                } else {
                    hresponse.setHeader("Cache-Control", "no-store");
                    hresponse.setHeader("Pragma", "no-cache");
                    hresponse.setDateHeader("Expires", 0);
                }

                if(data.getContentType() != null && !data.getContentType().contains("unknown")) {
                    hresponse.setContentType(data.getContentType());
                }
                hresponse.setContentLength(data.getLength());

                hresponse.getOutputStream().write(data.getData());
                hresponse.getOutputStream().flush();
                hresponse.getOutputStream().close();
            } else {
                chain.doFilter(request, response);
            }



        } else {
            chain.doFilter(request, response);
        }

        duration = System.currentTimeMillis() - starttime;
        if (log.isDebugEnabled()) {
            log.debug("request for {} took {}ms",url,duration);
        }
    }

    /**
     * Called by the web container to indicate to a filter that it is being taken out of service. This
     * method is only called once all threads within the filter's doFilter method have exited or after
     * a timeout period has passed. After the web container calls this method, it will not call the
     * doFilter method again on this instance of the filter. <br><br>
     * <p/>
     * This method gives the filter an opportunity to clean up any resources that are being held (for
     * example, memory, file handles, threads) and make sure that any persistent state is synchronized
     * with the filter's current state in memory.
     */
    @Override
    public void destroy() {
        //To change body of implemented methods use File | Settings | File Templates.
    }






    private ResourceCacheEntry getFromCache(String key) {
    	if(caching) {
    		return (ResourceCacheEntry)data_cache.get(key).getObjectValue();
    	} else {
    		return null;
    	}
    }


	// Store in the cache
    private void putInCache(String key, ResourceCacheEntry data) {
    	data_cache.put(new Element(key,data));
    }

    private String getMimeType(URL resource) {

    	Collection<MimeType> types = mimeUtil.getMimeTypes(resource);

    	if(types.size() > 0) {
    		MimeType t = types.iterator().next();
    		return t.toString();
    	} else {
    		return null;
    	}

    }

    private static class ResourceCacheEntry {
        byte[] data;
        int length;
        String contentType;

        public ResourceCacheEntry(byte[] data, int length, String contentType) {
            super();
            this.data = data;
            this.length = length;
            this.contentType = contentType;
        }

        /**
         * @return the data
         */
        public byte[] getData() {
            return data;
        }

        /**
         * @param data the data to set
         */
        public void setData(byte[] data) {
            this.data = data;
        }

        /**
         * @return the length
         */
        public int getLength() {
            return length;
        }

        /**
         * @param length the length to set
         */
        public void setLength(int length) {
            this.length = length;
        }

        /**
         * @return the contentType
         */
        public String getContentType() {
            return contentType;
        }

        /**
         * @param contentType the contentType to set
         */
        public void setContentType(String contentType) {
            this.contentType = contentType;
        }


    }

}
